## 实验二十三 TM1650四位数码管模块

**![39-数码管](media/f698ea56391906278b7c8064fca42bb3.jpeg)**

### 🌟 项目简介  
本实验带你用 Raspberry Pi Pico 控制 Keyes TM1650 四位数码管模块，实现从 `0` 到 `9999` 的自动计数显示。数码管每 0.01 秒（10 毫秒）加 1，循环滚动，直观展现数字变化过程。TM1650 是一款高度集成的 LED 驱动芯片，仅需 **2 根信号线（CLK 和 DIO）** 即可控制 4 位数码管，不占用 I²C 总线，也不需要额外的限流电阻，非常适合初学者快速上手。

> 💡 小知识：TM1650 的通信协议 *类似* I²C，但**不是标准 I²C**——它没有地址应答（ACK）时序中的“释放SDA”动作，而是靠主控主动拉低/释放来模拟，因此我们使用 **bit-banging（位操作）方式** 手动模拟时序，更稳定、更可控。

---

### ⚙️ 工作原理  

TM1650 内部集成了驱动电路、数据锁存器和键盘扫描功能，本实验只使用其 **LED 显示功能**。  
通信采用 **双线串行协议**（CLK 时钟线 + DIO 数据线），通过特定指令控制：

- **起始信号（START）**：DIO 从高→低，CLK 保持高电平  
- **停止信号（STOP）**：DIO 从低→高，CLK 保持高电平  
- **字节发送**：高位在前，每发 1 位，CLK 先拉低 → 等待 → 拉高 → 等待  
- **命令地址 `0x48`**：表示接下来要写入的是「显示控制命令」（不是按键读取）  
- **显示命令字节格式**（1 字节）：  
  `B B B D 0 0 0 E`  
  - `B B B`（bit6~bit4）：亮度等级（000=最亮，111=最暗）  
  - `D`（bit3）：小数点开关（1=点亮该位小数点）  
  - `E`（bit0）：显示使能（1=开启显示，0=关闭）  

![](media/f3494fd798de81b65f250c7592c6fba1.png)  
**数据命令设置：`0x48`（告诉芯片：“我要控制数码管显示！”）**

**![58bc695a093d4455b2035dcf8d6ff48f](media/6e4b9b3cf2d484e2c0389fb80fee107c.png)**  
**显示命令设置详解（1 字节）：**  
- bit6~bit4：亮度（000 最亮 → 111 最暗）  
- bit3：小数点（1 = 亮）  
- bit0：显示开关（1 = 开）

---

### 🧰 所需材料  

| ![rpi_pico-smd_1_breadboard](media/c4bec8956b5785f9b6c98b310e182d18.png) | ![KS3017 pico扩展板](media/b9233c67c93c243214388d668afe2eab.png) | ![KE4060](media/61c36ebc6656bef0257d60227515d947.png) | ![QQ图片20220426165719](media/178ac5a6046142ac33d08d63ead5c7ad.jpeg) | ![USB线](media/edbfec59fe015bd9987e4b4d542b466d.png) |
|--------------------------------------------------------------------------|------------------------------------------------------------------|-------------------------------------------------------|----------------------------------------------------------------------|------------------------------------------------------|
| Raspberry Pi Pico 主板 ×1                                               | Keyes Pico 扩展板 ×1（带防反插接口，接线更安全）                  | Keyes TM1650 四位数码管模块 ×1                         | 防反插 4Pin 杜邦线（含红/黑/黄/蓝）×1                                 | Micro USB 数据线 ×1（用于供电+下载程序）            |

✅ 提示：模块背面已标注引脚——  
- `VCC` → 接 3.3V（⚠️ 不要接 5V！Pico IO 口耐压仅 3.3V）  
- `GND` → 接 GND  
- `CLK` → 接 GP15（默认，可改）  
- `DIO` → 接 GP14（默认，可改）

---

### 🔌 接线图  

**![Untitled Sketch_bb](media/bb9c2df6be74f4abdcc00ad4eafdd2a9.jpeg)**  

📌 **正确接线口诀（请对照图检查）：**  
- TM1650 `VCC` → 扩展板 `3V3`（或 Pico 的 `VSYS` 也可，但推荐 `3V3`）  
- TM1650 `GND` → 扩展板 `GND`  
- TM1650 `CLK` → 扩展板 `GP15`（即 Pico 的第 20 脚）  
- TM1650 `DIO` → 扩展板 `GP14`（即 Pico 的第 19 脚）  

> ✅ 安全提醒：  
> - Pico 的 GPIO 口最大输出电流约 12mA，TM1650 模块内部已集成限流电阻，**无需外接电阻**；  
> - 请务必确认 `VCC` 接的是 `3V3`，接错 `5V` 可能永久损坏 Pico！

---

### 💻 示例代码（MicroPython）

```python
# Keyes Starter Kit for Raspberry Pi Pico
# 课程 23: TM1650 Four-digit Digital Tube
# 作者：Keyes 教育团队｜适配 MicroPython v1.22+

from machine import Pin
import time

# === TM1650 寄存器地址定义 ===
ADDR_DIS = 0x48  # 显示模式命令地址
ADDR_KEY = 0x49  # 按键读取命令地址（本实验未使用）

# === 亮度等级（0~7，0=最亮，7=最暗）===
BRIGHT_DARKEST = 0
BRIGHT_TYPICAL = 2
BRIGHTEST = 7

# === 数码管段码表（共阳，0~9）===
# 顺序：a b c d e f g dp → 对应 0x3F 中的每一位
NUM = [0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f]

# === 位选地址（从左到右：千位→百位→十位→个位）===
# 注意：TM1650 规定：0x68=第4位（千位），0x6a=第3位（百位），0x6c=第2位（十位），0x6e=第1位（个位）
# 我们按「显示顺序」映射为 DIG[0]→千位，DIG[1]→百位…所以反向排列：
DIG = [0x6e, 0x6c, 0x6a, 0x68]  # DIG[0]=个位，DIG[1]=十位，DIG[2]=百位，DIG[3]=千位

# === 小数点状态（默认全关）===
DOT = [0, 0, 0, 0]  # DOT[0]对应个位小数点，依此类推

# === 引脚定义（可自由修改！）===
clkPin = 15  # CLK 接 GP15（物理引脚 20）
dioPin = 14  # DIO 接 GP14（物理引脚 19）

clk = Pin(clkPin, Pin.OUT, value=1)
dio = Pin(dioPin, Pin.OUT, value=1)

# === 全局显示命令字（初始：亮度=2，无小数点，显示开启）===
DisplayCommand = (BRIGHT_TYPICAL << 4) | 0x01  # bit6~4=010，bit0=1 → 0x21

# === 时序函数：发送1字节数据 ===
def writeByte(wr_data):
    global clk, dio
    for i in range(8):
        if wr_data & 0x80:
            dio.value(1)
        else:
            dio.value(0)
        clk.value(0)
        time.sleep_us(1)
        clk.value(1)
        time.sleep_us(1)
        clk.value(0)
        wr_data <<= 1

# === 起始信号 ===
def start():
    global clk, dio
    dio.value(1)
    clk.value(1)
    time.sleep_us(1)
    dio.value(0)
    time.sleep_us(1)

# === 应答信号（TM1650 拉低DIO表示收到）===
def ack():
    global clk, dio
    clk.value(0)
    time.sleep_us(1)
    dio = Pin(dioPin, Pin.IN)  # 切换为输入
    # 等待芯片拉低 DIO（最多等 5ms，防死锁）
    for _ in range(5000):
        if dio.value() == 0:
            break
        time.sleep_us(1)
    clk.value(1)
    time.sleep_us(1)
    clk.value(0)
    dio = Pin(dioPin, Pin.OUT, value=1)  # 切回输出

# === 停止信号 ===
def stop():
    global clk, dio
    dio.value(0)
    clk.value(1)
    time.sleep_us(1)
    dio.value(1)
    time.sleep_us(1)

# === 在指定位置显示单个数字（bit=1~4，num=0~9）===
def displayBit(bit, num):
    global ADDR_DIS, DisplayCommand, DIG, NUM, DOT
    if bit < 1 or bit > 4 or num < 0 or num > 9:
        return
    # 第一步：发送显示模式命令
    start()
    writeByte(ADDR_DIS)
    ack()
    writeByte(DisplayCommand)
    ack()
    stop()
    # 第二步：发送位选地址 + 段码
    start()
    writeByte(DIG[bit-1])  # bit-1 → 数组索引（1→0, 2→1...）
    ack()
    if DOT[bit-1]:
        writeByte(NUM[num] | 0x80)  # 加上小数点（dp位=bit7）
    else:
        writeByte(NUM[num])
    ack()
    stop()

# === 清除某一位显示（显示为全灭）===
def clearBit(bit):
    if bit < 1 or bit > 4:
        return
    start()
    writeByte(ADDR_DIS)
    ack()
    writeByte(DisplayCommand)
    ack()
    stop()
    start()
    writeByte(DIG[bit-1])
    ack()
    writeByte(0x00)  # 全灭
    ack()
    stop()

# === 设置亮度（0~7）===
def setBrightness(b=2):
    global DisplayCommand
    DisplayCommand = (DisplayCommand & 0x0f) | ((b & 0x07) << 4)

# === 设置小数点（bit=1~4，OnOff=0或1）===
def displayDot(bit, OnOff):
    if bit < 1 or bit > 4:
        return
    if OnOff:
        DOT[bit-1] = 1
    else:
        DOT[bit-1] = 0

# === 开启/关闭整个数码管显示 ===
def displayOnOFF(OnOff=1):
    global DisplayCommand
    if OnOff:
        DisplayCommand |= 0x01
    else:
        DisplayCommand &= 0xfe

# === 初始化数码管：设亮度、关小数点、开启显示、清屏 ===
def initDigitalTube():
    setBrightness(2)      # 中等亮度
    displayOnOFF(1)       # 开启显示
    for i in range(4):    # 清除全部4位
        clearBit(i+1)

# === 显示一个0~9999的整数（自动补零，如 7 → "0007"）===
def showNum(num):
    if num < 0:
        num = 0
    if num > 9999:
        num = 9999
    # 个位
    displayBit(1, num % 10)
    # 十位
    if num >= 10:
        displayBit(2, (num // 10) % 10)
    # 百位
    if num >= 100:
        displayBit(3, (num // 100) % 10)
    # 千位
    if num >= 1000:
        displayBit(4, num // 1000)

# === 主程序开始 ===
initDigitalTube()

while True:
    for i in range(0, 10000):
        showNum(i)
        time.sleep_ms(10)  # 每次显示10毫秒，实现0.01秒刷新
```

---

### 📝 代码解析（小学生也能懂！）

| 代码片段 | 说明 |
|----------|------|
| `NUM = [0x3f, 0x06, ...]` | 这是「数字形状密码表」：比如 `0x3f` 表示数字 `0` 的亮段（a~g 全亮），`0x06` 表示数字 `1`（只亮 b、c 段）……就像给每个数字发一张“灯亮清单”。 |
| `DIG = [0x6e, 0x6c, 0x6a, 0x68]` | 这是「位置密码表」：`0x6e`=个位，`0x6c`=十位……告诉芯片“我要在哪个位置贴哪张清单”。 |
| `displayBit(1, 5)` | 就像说：“请在**个位**（bit=1）显示数字 **5**！” |
| `showNum(123)` | 自动拆解成：个位=3、十位=2、百位=1、千位=0 → 分别调用 `displayBit()`，不用自己算！ |
| `time.sleep_ms(10)` | 让每个数字“停留”10 毫秒，人眼看起来就是流畅滚动（太快会闪烁，太慢像幻灯片）。 |

> ✅ 小技巧：想让数字变亮？把 `setBrightness(2)` 改成 `setBrightness(0)`；想关掉显示？加一行 `displayOnOFF(0)`。

---

### ✅ 实验现象  

接线无误、程序烧录成功后：  
✅ 上电瞬间，4 位数码管短暂全亮 → 快速清屏 → 开始从 `0000` 缓慢递增；  
✅ 每 0.01 秒跳 1 个数字（`0000` → `0001` → `0002` … `9999` → `0000` 循环）；  
✅ 若某位不亮，请检查 `DIG` 数组顺序、`NUM` 段码是否匹配共阳极、或 `VCC` 是否接对 3.3V。

![57](media/29fc15d353d612c1728181ae67fbb8f8.jpeg)

---

### ⚠️ 注意事项（安全第一！）

- 🔌 **电源必须用 `3V3`！** TM1650 模块标称 3.3V～5.5V，但 Pico 的 GPIO 口只能承受 3.3V，若将模块 `VCC` 接到 `5V` 引脚，可能击穿 Pico！  
- 🧩 **模块方向别接反**：查看模块丝印，`VCC` 在左上角，`GND` 在右上角（与扩展板接口一致）；  
- 🐞 **常见问题排查**：  
  - 全黑？→ 检查 `VCC/GND` 是否接反、`displayOnOFF(1)` 是否执行、`DisplayCommand` 最低位是否为 1；  
  - 乱码？→ 检查 `NUM` 数组是否抄错、`DIG` 顺序是否与硬件位选匹配；  
  - 卡住不动？→ 检查 `CLK/DIO` 是否接错引脚，或 `time.sleep_ms(10)` 是否被误删。

---

### 🧠 扩展思维  
如果想让数码管显示当前时间（小时:分钟），而不是从 0 开始计数，你打算怎么修改 `showNum()` 和主循环？（提示：可用 `time.localtime()` 获取系统时间）